package org.nvframe.event;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.nvframe.component.Component;
import org.nvframe.entity.Entity;
import org.nvframe.event.eventtype.*;
import org.nvframe.exception.NVFrameException;

/**
 * 
 * @author Nik Van Looy
 */
public class EventService {
	
	private static EventService _instance = new EventService();

	public static EventService getInstance() {
		return _instance;
	}
	
	private Map<Entity, ArrayList<EventListener>> entityListeners;
	private List<EventListener> listenersWithOwner;
	private List<EventListener> listenersWithoutOwner;
	private List<EventFiredListener> eventServiceObservers;
	
	private EventService() {
		entityListeners = new HashMap<Entity, ArrayList<EventListener>>();
		listenersWithOwner = new ArrayList<EventListener>();
		listenersWithoutOwner = new ArrayList<EventListener> ();
		eventServiceObservers = new ArrayList<EventFiredListener> ();
	}
	
	/**
	 * Add an eventListener
	 * any class that implements EventListener can be added
	 * 
	 * @param listener
	 */
	public void addEventListener(EventListener listener) {
		Entity owner;
		
		if(listener instanceof EventFiredListener) {
			// listeners that want to capture all events fired in the eventService
			eventServiceObservers.add((EventFiredListener) listener);
			return;
		}
		else if(listener instanceof Entity)
			owner = (Entity) listener;
		else if(listener instanceof Component)
			owner = ((Component) listener).getOwner();
		else { 
			// classes that have nothing to do whith entities nor components
			listenersWithoutOwner.add(listener);
			return;
		}

		listenersWithOwner.add(listener);
		
		if(!entityListeners.containsKey(owner))
			entityListeners.put(owner, new ArrayList<EventListener>());
		
		entityListeners.get(owner).add(listener);
	}
	
	/**
	 * Remove a specified eventListener
	 * 
	 * @param listener
	 */
	public void removeEventListener(EventListener listener) {
		Entity owner;
		
		if(listener instanceof EventFiredListener) {
			// remove EventService observer
			if(eventServiceObservers.contains(listener))
				eventServiceObservers.remove(listener);
			return;
		}
		else if(listener instanceof Entity)
			owner = (Entity) listener;
		else if(listener instanceof Component)
			owner = ((Component) listener).getOwner();
		else
			owner = null;
		
		if(entityListeners.containsKey(owner) && entityListeners.get(owner).contains(listener))
			entityListeners.get(owner).remove(listener);
		
		if(listenersWithOwner.contains(listener))
			listenersWithOwner.remove(listener);
	}
	
	/**
	 * Fires a specified event to all listeners
	 * 
	 * @param event The event to pass to the listener(s)
	 */
	public void fireEvent(Event event) {
		// avoid ConcurrentModificationException
		ArrayList<EventListener> listenersColCpy = new ArrayList<EventListener> ();

		listenersColCpy.addAll(listenersWithOwner);
		listenersColCpy.addAll(listenersWithoutOwner);
		
		fireEvent(event, listenersColCpy);
		
		// notify EventService observers
		dispatchEventToObservers(event);
	}

	/**
	 * Fires a specified event
	 * 
	 * @param event The event to pass to the listener(s)
	 * @param mustHaveOwner Specifies that the Listener Object must be a subclass from Component or Entity
	 */
	public void fireEvent(Event event, boolean mustHaveOwner) {
		ArrayList<EventListener> listenersColCpy;
		
		if(mustHaveOwner)
			listenersColCpy = new ArrayList<EventListener> (listenersWithOwner);
		else
			listenersColCpy = new ArrayList<EventListener> (listenersWithoutOwner);
		
		fireEvent(event, listenersColCpy);
		
		// notify EventService observers
		dispatchEventToObservers(event);
	}
	
	/**
	 * Fires an event to a specified Entity object
	 * 
	 * @param event The event to pass to the listener(s)
	 * @param target The target Entity to fire the event to
	 */
	public void fireEvent(Event event, Entity target) {
		if(!target.isEnabled())
			return;
		
		ArrayList<EventListener> listenersColCpy = new ArrayList<EventListener> (entityListeners.get(target));
		fireEvent(event, listenersColCpy);
	}
	
	/**
	 * Fires an event to a specified collection of Listeners
	 * 
	 * @param event The Event that will be fired
	 * @param listeners The collections of EventListeners
	 */
	private void fireEvent(Event event, ArrayList<EventListener> listeners) {
		
		for(EventListener listener : listeners) {
			if(!event.hasListener(listener))
				continue;
			
			// listener is entity or component from entity? => check if the entity isnt disabled
			Entity owner = null;
			
			if(listener instanceof Entity)
				owner = (Entity) listener;
			else if(listener instanceof Component)
				owner = ((Component) listener).getOwner();
			
			if(owner != null)
				if(!owner.isEnabled())
					continue;
			
			try {
				dispatchEvent(event, listener);
			}
			catch(NVFrameException e) {
				e.printStackTrace();
			}
		}
	}
	
	/**
	 * 
	 * @param event The event that's beeing fired
	 */
	private void dispatchEventToObservers(Event event) {
		if(eventServiceObservers.size() == 0)
			return;
		
		// notify EventService observers of the fired event
		ArrayList<EventFiredListener> listenersCpy = new ArrayList<EventFiredListener> (eventServiceObservers);
		
		for(EventFiredListener esObserver : listenersCpy)
			esObserver.onEvent(event);
	}
	
	/**
	 * Fires the event
	 * this method does Type Casting so the implementation code
	 * can work with the correct concrete classes
	 * 
	 * @param event The event to pass to the listener(s)
	 * @param listener The listener to pass the event to
	 * @throws NVFrameException
	 */
	private void dispatchEvent(Event event, EventListener listener) throws NVFrameException {
		// TODO: exceptionhandling when listener cant be casted properly inside the event implementation
		event.dispatchToListener(listener);
	}
}
